From: Xiaofeng Wang Date: Mon, 30 Mar 2026 08:29:10 +0000 (+0800) Subject: ci: Replace test-tmt shell script with Rust xtask X-Git-Tag: archive/raspbian/2026.2-1+rpi1~1^2~10^2^2~5^2~1 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=00590b4b3676d7beb5a1de51da0299091265c28b;p=ostree.git ci: Replace test-tmt shell script with Rust xtask Replace the inline bash script in the Justfile test-tmt target with a Rust xtask crate (tests/xtask/) that handles TMT plan discovery, bcvk VM lifecycle, SSH readiness polling, and tmt invocation. This follows the bootc-dev/bootc cargo xtask run-tmt pattern. Also fix tests.fmf to pass test names individually with --exact, since libtest_mimic only accepts a single filter argument. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Xiaofeng Wang --- diff --git a/Justfile b/Justfile index fa8c7fec..8f96a7dc 100644 --- a/Justfile +++ b/Justfile @@ -115,84 +115,7 @@ integration-ephemeral *ARGS: # Each plan runs in its own VM for isolation, following the # bootc-dev/bootc cargo xtask run-tmt pattern. test-tmt *ARGS: build - #!/bin/bash - set -euo pipefail - image=localhost/ostree:latest - random_suffix=$RANDOM - - # Discover test plans - plans=$(tmt plan ls | grep '^/') - if [ -z "$plans" ]; then - echo "No test plans found" - exit 0 - fi - echo "Found test plans:" - echo "$plans" - - all_passed=true - for plan in $plans; do - plan_name=$(echo "$plan" | sed 's|.*/||; s|[^a-zA-Z0-9]|-|g') - vm_name="ostree-tmt-${random_suffix}-${plan_name}" - - echo "" - echo "========================================" - echo "Running plan: ${plan}" - echo "VM name: ${vm_name}" - echo "========================================" - - # Launch VM with bcvk - if ! bcvk libvirt run --name "${vm_name}" --detach "${image}"; then - echo "Failed to launch VM for plan ${plan}" >&2 - all_passed=false - continue - fi - - # Wait for SSH with a longer timeout (cloud-init first boot can be slow) - echo "Waiting for SSH on ${vm_name}..." - for i in $(seq 1 30); do - if bcvk libvirt ssh "${vm_name}" -- true 2>/dev/null; then - echo "SSH ready after ~$((i * 10))s" - break - fi - if [ "$i" = 30 ]; then - echo "Timeout waiting for SSH on ${vm_name}" >&2 - bcvk libvirt rm --stop --force "${vm_name}" 2>/dev/null || true - all_passed=false - continue 2 - fi - sleep 10 - done - - # Extract SSH connection details - inspect_json=$(bcvk libvirt inspect "${vm_name}" --format json) - ssh_port=$(echo "$inspect_json" | jq -r '.ssh_port') - ssh_key_file=$(mktemp) - echo "$inspect_json" | jq -r '.ssh_private_key' > "${ssh_key_file}" - chmod 600 "${ssh_key_file}" - - # Run tmt for this plan - if tmt run --id "${vm_name}" --all \ - provision --how connect \ - --guest localhost --user root \ - --port "${ssh_port}" --key "${ssh_key_file}" \ - plan --name "${plan}" \ - {{ARGS}}; then - echo "Plan ${plan} passed" - else - echo "Plan ${plan} failed" >&2 - all_passed=false - fi - - # Cleanup - rm -f "${ssh_key_file}" - bcvk libvirt rm --stop --force "${vm_name}" 2>/dev/null || true - done - - if [ "$all_passed" = false ]; then - echo "Some test plans failed" >&2 - exit 1 - fi - echo "All test plans passed" + cargo run --manifest-path tests/xtask/Cargo.toml -- run-tmt {{ARGS}} # Remove any leftover integration test VMs integration-cleanup: diff --git a/tests/xtask/.gitignore b/tests/xtask/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/tests/xtask/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/tests/xtask/Cargo.lock b/tests/xtask/Cargo.lock new file mode 100644 index 00000000..89bf647e --- /dev/null +++ b/tests/xtask/Cargo.lock @@ -0,0 +1,583 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ostree-xtask" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "serde_json", + "tempfile", + "xshell", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "xshell" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tests/xtask/Cargo.toml b/tests/xtask/Cargo.toml new file mode 100644 index 00000000..79569d45 --- /dev/null +++ b/tests/xtask/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ostree-xtask" +version = "0.1.0" +edition = "2021" +publish = false + +# Separate workspace from root; this is a dev-only tool +[workspace] + +[[bin]] +name = "ostree-xtask" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tempfile = "3" +xshell = "0.2" diff --git a/tests/xtask/src/main.rs b/tests/xtask/src/main.rs new file mode 100644 index 00000000..73841961 --- /dev/null +++ b/tests/xtask/src/main.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use clap::Parser; + +mod tmt; + +/// Development task runner for ostree integration testing. +#[derive(Debug, Parser)] +enum Opt { + /// Run TMT tests inside bcvk-deployed VMs. + /// + /// Each plan runs in its own VM for isolation, following the + /// bootc-dev/bootc cargo xtask run-tmt pattern. + RunTmt(tmt::RunTmtArgs), +} + +fn main() -> Result<()> { + let opt = Opt::parse(); + let sh = xshell::Shell::new()?; + match opt { + Opt::RunTmt(args) => tmt::run_tmt(&sh, args), + } +} diff --git a/tests/xtask/src/tmt.rs b/tests/xtask/src/tmt.rs new file mode 100644 index 00000000..460cc5e1 --- /dev/null +++ b/tests/xtask/src/tmt.rs @@ -0,0 +1,209 @@ +use std::io::Write; + +use anyhow::{Context, Result, bail}; +use clap::Parser; +use xshell::{Shell, cmd}; + +/// Maximum time to wait for SSH to become available (in seconds). +const SSH_TIMEOUT_SECS: u64 = 300; +/// Interval between SSH readiness checks (in seconds). +const SSH_POLL_INTERVAL_SECS: u64 = 10; + +#[derive(Debug, Parser)] +pub(crate) struct RunTmtArgs { + /// Container image to boot in VMs. + #[clap(long, default_value = "localhost/ostree:latest")] + image: String, + + /// Only run plans whose name contains one of these filters. + #[clap(long)] + filter: Vec, + + /// Extra arguments to pass to `tmt run`. + #[clap(last = true)] + tmt_args: Vec, +} + +/// Information extracted from `bcvk libvirt inspect`. +#[derive(serde::Deserialize)] +struct BcvkInspect { + ssh_port: u16, + ssh_private_key: String, +} + +pub(crate) fn run_tmt(sh: &Shell, args: RunTmtArgs) -> Result<()> { + check_dependencies(sh)?; + + let image = &args.image; + let plans = discover_plans(sh, &args.filter)?; + if plans.is_empty() { + eprintln!("No test plans found"); + return Ok(()); + } + eprintln!("Found {} test plan(s):", plans.len()); + for p in &plans { + eprintln!(" {p}"); + } + + let random_suffix: u16 = std::process::id() as u16; + let mut failures: Vec<(String, String)> = Vec::new(); + + for plan in &plans { + let plan_name = sanitize_plan_name(plan); + let vm_name = format!("ostree-tmt-{random_suffix}-{plan_name}"); + + eprintln!(); + eprintln!("========================================"); + eprintln!("Running plan: {plan}"); + eprintln!("VM name: {vm_name}"); + eprintln!("========================================"); + + match run_plan(sh, &args, image, plan, &vm_name) { + Ok(()) => eprintln!("Plan {plan} passed"), + Err(e) => { + eprintln!("Plan {plan} failed: {e:#}"); + failures.push((plan.clone(), format!("{e:#}"))); + } + } + + // Always clean up the VM + cleanup_vm(sh, &vm_name); + } + + eprintln!(); + if failures.is_empty() { + eprintln!("All {} test plan(s) passed", plans.len()); + Ok(()) + } else { + eprintln!( + "{} of {} plan(s) failed:", + failures.len(), + plans.len() + ); + for (plan, err) in &failures { + eprintln!(" {plan}: {err}"); + } + bail!("Some test plans failed"); + } +} + +/// Verify that required tools are available. +fn check_dependencies(sh: &Shell) -> Result<()> { + for tool in ["bcvk", "tmt"] { + cmd!(sh, "{tool} --version") + .quiet() + .ignore_stdout() + .run() + .with_context(|| format!("`{tool}` not found; is it installed?"))?; + } + Ok(()) +} + +/// Discover TMT plans and optionally filter them. +fn discover_plans(sh: &Shell, filters: &[String]) -> Result> { + let output = cmd!(sh, "tmt plan ls").read()?; + let plans: Vec = output + .lines() + .map(|l| l.trim().to_owned()) + .filter(|l| l.starts_with('/')) + .collect(); + + if filters.is_empty() { + return Ok(plans); + } + + Ok(plans + .into_iter() + .filter(|p| filters.iter().any(|f| p.contains(f))) + .collect()) +} + +/// Run a single TMT plan in a dedicated bcvk VM. +fn run_plan( + sh: &Shell, + args: &RunTmtArgs, + image: &str, + plan: &str, + vm_name: &str, +) -> Result<()> { + // Launch the VM + cmd!(sh, "bcvk libvirt run --name {vm_name} --detach {image}") + .run() + .context("Failed to launch VM")?; + + // Wait for SSH + wait_for_ssh(sh, vm_name)?; + + // Extract SSH connection details + let inspect_raw = cmd!(sh, "bcvk libvirt inspect {vm_name} --format json").read()?; + let inspect: BcvkInspect = + serde_json::from_str(&inspect_raw).context("Failed to parse bcvk inspect output")?; + + // Write SSH key to a temporary file + let mut key_file = tempfile::NamedTempFile::new()?; + key_file.write_all(inspect.ssh_private_key.as_bytes())?; + key_file.flush()?; + let key_path = key_file.path(); + + let ssh_port = inspect.ssh_port.to_string(); + let tmt_args = &args.tmt_args; + + // Run tmt with connect provisioner + cmd!( + sh, + "tmt run --id {vm_name} --all + provision --how connect + --guest localhost --user root + --port {ssh_port} --key {key_path} + plan --name {plan} + {tmt_args...}" + ) + .run() + .with_context(|| format!("tmt plan {plan} failed"))?; + + Ok(()) +} + +/// Wait for SSH to become available on a bcvk VM. +fn wait_for_ssh(sh: &Shell, vm_name: &str) -> Result<()> { + eprintln!("Waiting for SSH on {vm_name}..."); + let max_attempts = SSH_TIMEOUT_SECS / SSH_POLL_INTERVAL_SECS; + + for i in 1..=max_attempts { + if cmd!(sh, "bcvk libvirt ssh {vm_name} -- true") + .quiet() + .ignore_stdout() + .ignore_stderr() + .run() + .is_ok() + { + eprintln!("SSH ready after ~{}s", i * SSH_POLL_INTERVAL_SECS); + return Ok(()); + } + + std::thread::sleep(std::time::Duration::from_secs(SSH_POLL_INTERVAL_SECS)); + } + + bail!( + "Timeout waiting for SSH on {vm_name} after {SSH_TIMEOUT_SECS}s" + ); +} + +/// Clean up a bcvk VM, ignoring errors. +fn cleanup_vm(sh: &Shell, vm_name: &str) { + let _ = cmd!(sh, "bcvk libvirt rm --stop --force {vm_name}") + .quiet() + .ignore_stdout() + .ignore_stderr() + .run(); +} + +/// Sanitize a TMT plan name for use in a VM name. +fn sanitize_plan_name(plan: &str) -> String { + plan.rsplit('/') + .next() + .unwrap_or(plan) + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) + .collect() +} diff --git a/tmt/tests/tests.fmf b/tmt/tests/tests.fmf index 8fd478aa..c3e94140 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -1,9 +1,17 @@ /test-booted-verification: summary: Verify ostree system state on image-mode system duration: 15m - test: ostree-bootc-integration-tests privileged_verify_ostree_booted privileged_verify_sysroot privileged_verify_composefs privileged_verify_ostree_cli privileged_verify_sysroot_readonly privileged_verify_ostree_run_metadata privileged_verify_immutable_bit privileged_verify_osinit_unshare privileged_verify_selinux_labels + test: | + set -xeuo pipefail + for t in privileged_verify_ostree_booted privileged_verify_sysroot privileged_verify_composefs privileged_verify_ostree_cli privileged_verify_sysroot_readonly privileged_verify_ostree_run_metadata privileged_verify_immutable_bit privileged_verify_osinit_unshare privileged_verify_selinux_labels; do + ostree-bootc-integration-tests --exact "$t" + done /test-ostree-privileged: summary: Privileged ostree tests (nofifo, mtime, extensions) duration: 10m - test: ostree-bootc-integration-tests privileged_verify_nofifo privileged_verify_mtime privileged_verify_extensions + test: | + set -xeuo pipefail + for t in privileged_verify_nofifo privileged_verify_mtime privileged_verify_extensions; do + ostree-bootc-integration-tests --exact "$t" + done